// @todo: support hot reload
// @todo: support functional check
// @tips: not support CSS Modules
// @tips: not support CssSourceMap
// @tips: not support isServer
// @tips: not support isScoped

var loaderUtils = require('loader-utils');
var assign = require('object-assign');
var parse = require('./parser');
var path = require('path');
var normalize = require('./normalize');
var genId = require('./gen-id');

// internal lib loaders
var selectorPath = normalize.lib('selector');
var templateLoaderPath = normalize.lib('template-loader');
var templateCompilerPath = normalize.lib('template-compiler');
var styleRewriterPath = normalize.lib('style-rewriter');
var styleLoaderPath = normalize.lib('style-loader');
var scriptLoaderPath = normalize.lib('script-loader');

// dep loaders
// var styleLoaderPath = normalize.dep('vue-style-loader')
// var hotReloadAPIPath = normalize.dep('vue-hot-reload-api')

var hasBabel = false;
try {
    hasBabel = !!require('babel-loader')
} catch (e) {
}

var hasBuble = false;
try {
    hasBuble = !!require('buble-loader')
} catch (e) {
}

var rewriterInjectRE = /\b(css(?:-loader)?(?:\?[^!]+)?)(?:!|$)/;

var defaultLang = {
    template: 'html',
    styles: 'css',
    script: 'js'
};

var checkNamedExports =
    'if (Object.keys(__vue_exports__).some(function (key) { return key !== "default" && key !== "__esModule" })) {' +
    'console.error("named exports are not supported in *.vue files.")}\n';

function hasRecyclable(template) {
    return !!(template && template.attrs && template.attrs.recyclable)
}

module.exports = function (content) {
    this.cacheable();
    // var isServer = (this.options || this._compiler.options).target === 'node'
    // var isServer = false
    var loaderContext = this;
    var query = loaderUtils.getOptions(this) || {};
    var params = this.resourceQuery && loaderUtils.parseQuery(this.resourceQuery);
    var options = (this.options || this._compiler.options).__vueOptions__ = Object.assign({}, (this.options || this._compiler.options).vue, query);
    var filePath = this.resourcePath;
    var fileName = path.basename(filePath);
    var moduleId = 'data-v-' + genId(path.relative(process.cwd(), filePath));
    var styleRewriter = styleRewriterPath + '?id=' + moduleId;

    var isProduction = this.minimize || process.env.NODE_ENV === 'production';

    // var needCssSourceMap =
    //   !isProduction &&
    //   this.sourceMap &&
    //   options.cssSourceMap !== false
    // var needCssSourceMap = false

    var babelOptions = hasBabel && options.babel && options.babel.query ? '?' + JSON.stringify(options.babel.query) : '';
    var bubleOptions = hasBuble && options.buble ? '?' + JSON.stringify(options.buble) : '';
    var defaultLoaders = {
        html: templateCompilerPath + '?id=' + moduleId,
        // css: (isServer ? '' : styleLoaderPath + '!') + 'css-loader' + (needCssSourceMap ? '?sourceMap' : ''),
        css: styleLoaderPath,
        js: scriptLoaderPath + '!' + (hasBuble ? ('buble-loader' + bubleOptions) : hasBabel ? ('babel-loader' + babelOptions) : ''),
        ts: [{
            loader: 'ts-loader',
            options: {appendTsSuffixTo: [/\.vue$/]}
        }],
        scss: ['sass-loader'],
        sass: [{
            loader: 'sass-loader',
            options: {
                indentedSyntax: true
            }
        }],
        less: ['less-loader'],
        stylus: ['stylus-loader'],
        styl: ['stylus-loader']
    };

    // check if there are custom loaders specified via
    // webpack config, otherwise use defaults
    var loaders = assign({}, defaultLoaders, options.loaders);

    function getRequire(type, part, index, scoped) {
        return 'require(' +
            getRequireString(type, part, index, scoped) +
            ')\n'
    }

    function getRequireString(type, part, index, scoped) {
        return loaderUtils.stringifyRequest(loaderContext,
            // disable all configuration loaders
            '!!' +
            // get loader string for pre-processors
            getLoaderString(type, part, index, scoped) +
            // select the corresponding part from the vue file
            getSelectorString(type, index || 0) +
            // the url to the actual vuefile
            filePath
        )
    }

    function getRequireForImport(type, impt, scoped) {
        return 'require(' +
            getRequireForImportString(type, impt, scoped) +
            ')\n'
    }

    function getRequireForImportString(type, impt, scoped) {
        return loaderUtils.stringifyRequest(loaderContext,
            '!!' +
            getLoaderString(type, impt, -1, scoped) +
            impt.src
        )
    }

    function addCssModulesToLoader(loader, part, index) {
        if (!part.module) return loader;
        var option = options.cssModules || {};
        var DEFAULT_OPTIONS = {
            modules: true,
            importLoaders: true
        };
        var OPTIONS = {
            localIdentName: '[hash:base64]'
        };
        return loader.replace(/((?:^|!)css(?:-loader)?)(\?[^!]*)?/, function (m, $1, $2) {
            // $1: !css-loader
            // $2: ?a=b
            var query = loaderUtils.parseQuery($2);
            Object.assign(query, OPTIONS, option, DEFAULT_OPTIONS);
            if (index !== -1) {
                // Note:
                //   Class name is generated according to its filename.
                //   Different <style> tags in the same .vue file may generate same names.
                //   Append `_[index]` to class name to avoid this.
                query.localIdentName += '_' + index
            }
            return $1 + '?' + JSON.stringify(query)
        })
    }

    function stringifyLoaders(loaders) {
        return loaders.map(function (obj) {
            return obj && typeof obj === 'object' && typeof obj.loader === 'string'
                ? obj.loader + (obj.options ? '?' + JSON.stringify(obj.options) : '')
                : obj
        }).join('!')
    }

    function getLoaderString(type, part, index, scoped) {
        var lang = part.lang;
        var loader = loaders[defaultLang[type]];
        var rewriter = type === 'styles' ? styleRewriter + (scoped ? '&scoped=true!' : '!') : '';
        var injectString = (type === 'script' && query.inject) ? 'inject!' : '';
        if (loader !== undefined) {
            if (Array.isArray(loader)) {
                loader = stringifyLoaders(loader)
            }
            // add css modules
            if (type === 'styles') {
                loader = addCssModulesToLoader(loader, part, index);
                // while lang existed, the `style-loader` is also needed.
                if (lang && Array.isArray(loaders[lang])) {
                    loader += '!' + stringifyLoaders(loaders[lang])
                }
            }
            if (type === 'template') {
                if (hasRecyclable(part)) {
                    loader += '&recyclable=true'
                }
                if (lang) {
                    loader += '!' + templateLoaderPath + '?raw&engine=' + lang + '!'
                }
            }
            if (type === 'script') {
                if (lang && Array.isArray(loaders[lang])) {
                    loader += '!' + stringifyLoaders(loaders[lang])
                }
            }
            // inject rewriter before css/html loader for
            // extractTextPlugin use cases
            if (rewriterInjectRE.test(loader)) {
                loader = loader.replace(rewriterInjectRE, function (m, $1) {
                    return ensureBang($1) + rewriter
                })
            } else {
                loader = ensureBang(loader) + rewriter
            }
            return injectString + ensureBang(loader)
        } else {
            // unknown lang, infer the loader to be used
            switch (type) {
                case 'template':
                    return defaultLoaders.html + '!' + templateLoaderPath + '?raw&engine=' + lang + '!';
                case 'styles':
                    loader = addCssModulesToLoader(defaultLoaders.css, part, index);
                    return loader + '!' + rewriter + ensureBang(ensureLoader(lang));
                case 'script':
                    return injectString + ensureBang(ensureLoader(lang))
            }
        }
    }

    // sass => sass-loader
    // sass-loader => sass-loader
    // sass?indentedsyntax!css => sass-loader?indentedSyntax!css-loader
    function ensureLoader(lang) {
        return lang.split('!').map(function (loader) {
            return loader.replace(/^([\w-]+)(\?.*)?/, function (_, name, query) {
                return (/-loader$/.test(name) ? name : (name + '-loader')) + (query || '')
            })
        }).join('!')
    }

    function getSelectorString(type, index) {
        return selectorPath +
            '?type=' + type +
            '&index=' + index + '!'
    }

    function ensureBang(loader) {
        if (loader.charAt(loader.length - 1) !== '!') {
            return loader + '!'
        } else {
            return loader
        }
    }

    var parts = parse(content, fileName, this.sourceMap);
    // var hasGlobal = parts.styles.some(function (s) { return !!s.global })
    // var scopeId = hasGlobal ? '@GLOBAL' : ('data-v-' + genId(filePath))
    var scopeId = 'data-v-' + genId(path.relative(process.cwd(), filePath));
    var output = 'var __vue_exports__, __vue_options__\n';

    // css modules
    // output += 'var __vue_styles__ = {}\n'
    output += 'var __vue_styles__ = []\n';
    // var cssModules = {}

    // add requires for styles
    if (parts.styles.length) {
        output += '\n/* styles */\n';
        parts.styles.forEach(function (style, i) {
            // var moduleName = (style.module === true) ? '$style' : style.module
            // var moduleName

            // require style
            // if (isServer && !moduleName) return
            // var requireString = style.src
            //   ? getRequireForImport('styles', style, style.scoped)
            //   : getRequire('styles', style, i, style.scoped)
            var requireString = style.src
                ? getRequireForImport('styles', style)
                : getRequire('styles', style, i);

            // setCssModule
            // if (moduleName) {
            //   if (moduleName in cssModules) {
            //     loaderContext.emitError('CSS module name "' + moduleName + '" is not unique!')
            //     output += requireString
            //   } else {
            //     cssModules[moduleName] = true

            //     // `style-loader` exposes the name-to-hash map directly
            //     // `css-loader` exposes it in `.locals`
            //     // We drop `style-loader` in SSR, and add `.locals` here.
            //     // if (isServer) {
            //     //   requireString += '.locals'
            //     // }

            //     output += '__vue_styles__["' + moduleName + '"] = ' + requireString + '\n'
            //   }
            // } else {
            //   output += requireString
            // }

            // output += requireString
            output += '__vue_styles__.push(' + requireString + ')\n'
        })
    }

    // add require for script
    var script = parts.script;
    if (script) {
        output += '\n/* script */\n';
        output +=
            '__vue_exports__ = ' + (
                script.src
                    ? getRequireForImport('script', script)
                    : getRequire('script', script)
            )
    }

    var exports =
        '__vue_options__ = __vue_exports__ = __vue_exports__ || {}\n' +
        // ES6 modules interop
        'if (\n' +
        '  typeof __vue_exports__.default === "object" ||\n' +
        '  typeof __vue_exports__.default === "function"\n' +
        ') {\n' +
        (isProduction ? '' : checkNamedExports) +
        '__vue_options__ = __vue_exports__ = __vue_exports__.default\n' +
        '}\n' +
        // constructor export interop
        'if (typeof __vue_options__ === "function") {\n' +
        '  __vue_options__ = __vue_options__.options\n' +
        '}\n' +
        // add filename in dev
        (isProduction ? '' : ('__vue_options__.__file = ' + JSON.stringify(filePath))) + '\n';

    // add require for template
    var template = parts.template;
    if (template) {
        output += '\n/* template */\n';
        output += 'var __vue_template__ = ' + (
            template.src
                ? getRequireForImport('template', template)
                : getRequire('template', template)
        );
        // attach render functions to exported options
        exports +=
            '__vue_options__.render = __vue_template__.render\n' +
            (hasRecyclable(template) ? '__vue_options__["@render"] = __vue_template__["@render"]\n' : '') +
            '__vue_options__.staticRenderFns = __vue_template__.staticRenderFns\n'
    }

    // attach scoped id
    if (parts.styles.length) {
        exports += '__vue_options__._scopeId = "' + scopeId + '"\n'
    }

    // if (Object.keys(cssModules).length) {
    //   // inject style modules as computed properties
    //   exports +=
    //     'if (!__vue_options__.computed) __vue_options__.computed = {}\n' +
    //     'Object.keys(__vue_styles__).forEach(function (key) {\n' +
    //       'var module = __vue_styles__[key]\n' +
    //       '__vue_options__.computed[key] = function () { return module }\n' +
    //     '})\n'
    // }

    exports += '__vue_options__.style = __vue_options__.style || {}\n' +
        '__vue_styles__.forEach(function (module) {\n' +
        '  for (var name in module) {\n' +
        '    __vue_options__.style[name] = module[name]\n' +
        '  }\n' +
        '})\n';

    // support to register static styles
    exports += 'if (typeof __register_static_styles__ === "function") {\n' +
        '  __register_static_styles__(__vue_options__._scopeId, __vue_styles__)\n' +
        '}\n';

    if (!query.inject) {
        output += exports;
        // hot reload
        // if (
        //   !isServer &&
        //   !isProduction &&
        //   (parts.script || parts.template)
        // ) {
        //   output +=
        //     '\n/* hot reload */\n' +
        //     'if (module.hot) {(function () {\n' +
        //     '  var hotAPI = require("' + hotReloadAPIPath + '")\n' +
        //     '  hotAPI.install(require("vue"), false)\n' +
        //     '  if (!hotAPI.compatible) return\n' +
        //     '  module.hot.accept()\n' +
        //     '  if (!module.hot.data) {\n' +
        //     // initial insert
        //     '    hotAPI.createRecord("' + moduleId + '", __vue_options__)\n' +
        //     '  } else {\n' +
        //     // update
        //     '    hotAPI.reload("' + moduleId + '", __vue_options__)\n' +
        //     '  }\n' +
        //     '})()}\n'
        // }
        // check functional
        // if (!isProduction) {
        //   output +=
        //     'if (__vue_options__.functional) {console.error("' +
        //       '[vue-loader] ' + fileName + ': functional components are not ' +
        //       'supported and should be defined in plain js files using render ' +
        //       'functions.' +
        //     '")}\n'
        // }
        // final export
        if (options.esModule) {
            output += '\nexports.__esModule = true;\nexports["default"] = __vue_exports__\n'
        } else {
            output += '\nmodule.exports = __vue_exports__\n'
        }
    } else {
        // inject-loader support
        output +=
            '\n/* dependency injection */\n' +
            'module.exports = function (injections) {\n' +
            '  __vue_exports__ = __vue_exports__(injections)\n' +
            exports +
            '  return __vue_exports__\n' +
            '}'
    }

    if (params && params.entry) {
        output += 'module.exports.el = \'' + params.entry + '\'\n' +
            'new Vue(module.exports)\n'
    }

    // done
    return output
};
